/*
* Copyright 2009 NOR_/D Labs <http://labs.nortd.com>
*
* This file is part of SceneExpression.
* SceneExpression is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For details
* see <http://www.gnu.org/licenses/>.
*
* * *
*
*/

#include <osgDB/ReaderWriter>
#include <osgDB/FileUtils>

#include <sx/Scene.h>
#include <sx/Node.h>
#include <sx/Widget.h>

using namespace sx;


App::App() {
    scene = new Scene();
    scene->addEventHandler(this);
    //scene->setBackgroundColor(1,1,1);
}

App::~App() {
    delete scene;
}

int App::run() {
    return scene->run();
}

bool App::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa) {
    switch(ea.getEventType()) {
        // app
        //
        case(osgGA::GUIEventAdapter::RESIZE):
            resize(ea.getWindowWidth(), ea.getWindowHeight());
            return true;

        case(osgGA::GUIEventAdapter::QUIT_APPLICATION):
            this->exit();
            return true;

        // key
        //
        case(osgGA::GUIEventAdapter::KEYDOWN):
            _keyEvent.setKey(ea.getKey());
            keyPress(_keyEvent);
            return true;
        case(osgGA::GUIEventAdapter::KEYUP):
            _keyEvent.setKey(ea.getKey());
            keyRelease(_keyEvent);
            return true;

        // mouse
        //
        case(osgGA::GUIEventAdapter::MOVE):
            _mouseEvent.setX(ea.getX());
            _mouseEvent.setY(ea.getY());
            mouseMove(_mouseEvent);
            return true;
        case(osgGA::GUIEventAdapter::DRAG):
            _mouseEvent.setX(ea.getX());
            _mouseEvent.setY(ea.getY());
            _mouseEvent.setButton(ea.getButton());
            scene->mouseDrag(_mouseEvent.getX(), _mouseEvent.getY(), _mouseEvent.getButton());  //inject into widget system
            mouseDrag(_mouseEvent);
            return true;
        case(osgGA::GUIEventAdapter::PUSH):
            _mouseEvent.setX(ea.getX());
            _mouseEvent.setY(ea.getY());
            _mouseEvent.setButton(ea.getButton());
            scene->mousePress(_mouseEvent.getX(), _mouseEvent.getY(), _mouseEvent.getButton());  //inject into widget system
            mousePress(_mouseEvent);
            return true;
        case(osgGA::GUIEventAdapter::RELEASE):
            _mouseEvent.setX(ea.getX());
            _mouseEvent.setY(ea.getY());
            _mouseEvent.setButton(ea.getButton());
            scene->mouseRelease(_mouseEvent.getX(), _mouseEvent.getY(), _mouseEvent.getButton());  //inject into widget system
            mouseRelease(_mouseEvent);
            return true;
        case(osgGA::GUIEventAdapter::DOUBLECLICK):
            _mouseEvent.setX(ea.getX());
            _mouseEvent.setY(ea.getY());
            _mouseEvent.setButton(ea.getButton());
            mouseDoubleclick(_mouseEvent);
            return true;
        case(osgGA::GUIEventAdapter::SCROLL):
            _mouseEvent.setScrollingMotionDelta(ea.getScrollingDeltaX(), ea.getScrollingDeltaY());
            mouseScroll(_mouseEvent);
            return true;

        // pen
        //
        case(osgGA::GUIEventAdapter::PEN_PRESSURE):
            _penEvent.setX(ea.getX());
            _penEvent.setY(ea.getY());
            _penEvent.setPressure(ea.getPenPressure());
            penPressure(_penEvent);
            return true;
        case(osgGA::GUIEventAdapter::PEN_ORIENTATION):
            _penEvent.setX(ea.getX());
            _penEvent.setY(ea.getY());
            _penEvent.setTiltX(ea.getPenTiltX());
            _penEvent.setTiltY(ea.getPenTiltY());
            _penEvent.setRotation(ea.getPenRotation());
            penPressure(_penEvent);
            return true;
        case(osgGA::GUIEventAdapter::PEN_PROXIMITY_ENTER):
            _penEvent.setX(ea.getX());
            _penEvent.setY(ea.getY());
            penEnter(_penEvent);
            return true;
        case(osgGA::GUIEventAdapter::PEN_PROXIMITY_LEAVE):
            _penEvent.setX(ea.getX());
            _penEvent.setY(ea.getY());
            penLeave(_penEvent);
            return true;


        default:
            return false;
    }
}




Scene::Scene() {
    theMouseTouch.bMouse = true;

    // create the viewer of the scene.
    _viewer = new osgViewer::Viewer;

    // the viewer needs a graphic context
    // OSG has a few convenience functions for this like: setUpViewOnSingleScreen(0)
    // alternatively this can be done manually like in the following block
    // This also figures out the screen resolution of the system and
    // set the camera up accordingly. With the WindowingSystemInterface
    // object we can also request a change of screen resolution with: setScreenResolution(...)
    osg::GraphicsContext::WindowingSystemInterface* wsi = osg::GraphicsContext::getWindowingSystemInterface();
    if (!wsi) {
        osg::notify(osg::FATAL)<<"no WindowSystemInterface available"<<std::endl;
    }
    // get width, height of the system/screen
    wsi->getScreenResolution(osg::GraphicsContext::ScreenIdentifier(0), _width, _height);
    osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
    traits->x = 0;
    traits->y = 0;
    traits->width = _width;
    traits->height = _height;
    traits->windowDecoration = true;
    traits->doubleBuffer = true;
    traits->sharedContext = 0;
    osg::ref_ptr<osg::GraphicsContext> gc = osg::GraphicsContext::createGraphicsContext(traits.get());
    if (gc.valid()) {
        osg::notify(osg::INFO)<<"GraphicsWindow successfully created."<<std::endl;
        // clear window in case camera viewport do not cover it all
        gc->setClearColor(osg::Vec4f(0.2f,0.2f,0.6f,1.0f));
        gc->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    } else {
        osg::notify(osg::NOTICE)<<"Failed to create GraphicsWindow."<<std::endl;
    }

    // create a root node for the scene
    _root = new osg::PositionAttitudeTransform();
    _root->setName("scene");
    _geode = new osg::Geode();
    _root->addChild(_geode.get());
    _viewer->setSceneData(_root.get());

    // get the master camera and set it up
    _camera = _viewer->getCamera();
    setupPixelCamera();

    setBackgroundColor(0.1568, 0.1804, 0.2431);

    // anti-alias by default
    // osg::DisplaySettings::instance()->setNumMultiSamples(4);

    // no default lights
    // _viewer->setLightingMode(osg::View::NO_LIGHT);


    osgDB::FilePathList& dataFilePathList = osgDB::getDataFilePathList();
    dataFilePathList.push_back("../../data/");     // OSX cmake bundle, VS9
    dataFilePathList.push_back("data/");
    dataFilePathList.push_back("../data/");
    dataFilePathList.push_back("../../../data/");
    // dataFilePathList.push_back("../../../../apps/sceneExpressionExamples/shapesExample/data/");
    osgDB::setDataFilePathList(dataFilePathList);
    // std::cout << getcwd(NULL,0) << std::endl;
    std::cout << "dataFilePathList" << std::endl;
    for( unsigned int i=0; i<dataFilePathList.size(); i++ ) {
        std::cout << dataFilePathList[i] << std::endl;
    }
}


void Scene::initForGlutLegacy() {
    theMouseTouch.bMouse = true;
    // create the view of the scene.
    _viewer = new osgViewer::Viewer;
    _window = _viewer->setUpViewerAsEmbeddedInWindow(0,0,_width,_height);
    //_viewer->setCameraManipulator(new osgGA::TrackballManipulator);
    //_viewer->addEventHandler(new osgViewer::StatsHandler);
    _viewer->realize();

    //create a root node for the scene
    _root = new osg::PositionAttitudeTransform();
    _root->setName("scene");
    _geode = new osg::Geode();
    _root->addChild(_geode.get());
    _viewer->setSceneData(_root.get());

    if( _window->getCameras().size() > 0 ) {
        _camera  = *(_window->getCameras().begin());
        setupPixelCamera();
    } else {
        osg::notify(osg::FATAL) << "in Scene(), no cameras found" << std::endl;
    }

    setBackgroundColor(0.1568, 0.1804, 0.2431);
}

void Scene::setupPixelCamera() {
    //set up the camera so one pixel is one unit

    double halfFov, theTan, screenFov, aspect;
    screenFov 		= 45.0;

    double eyeX 	 = (double)_width / 2.0;
    double eyeY 	 = (double)_height / 2.0;
    halfFov 		 = osg::PI * screenFov / 360.0;
    theTan 			 = tanf(halfFov);
    double dist 	 = eyeY / theTan;
    double nearDist  = dist / 10.0;	  // near/far clip plane
    double farDist 	 = dist * 10.0;
    aspect 			 = (double)_width/(double)_height;

    _camera->setProjectionMatrixAsPerspective(screenFov, aspect, nearDist, farDist);
    _camera->setViewMatrixAsLookAt(osg::Vec3d(eyeX, eyeY, dist),  //cam pos
                                   osg::Vec3d(eyeX, eyeY, 0.0),   //look at pos
                                   osg::Vec3d(0.0, 1.0, 0.0));    //up vector

    // Origin
    // The origin is at the bottom left, X-axis is left to right
    // Y-axis bottom to top, Z-axis is into the screen.
    // This is consisten with OSG and OpenGL.
    // Fllip y axis is possible with the following lines but be warned
    // this does not flip anything texture based like text.
    // root->setScale(osg::Vec3(1.0,-1.0,1.0));
    // root->setPosition(osg::Vec3(0.0,h,0.0));

    // Clipping Planes
    // by default near and far clipping planes are auto-computed
    // based on the global bounding sphere, in rare cases it might
    // be necessary to switch this off with the following line
    // camera->setComputeNearFarMode(osg::CullSettings::DO_NOT_COMPUTE_NEAR_FAR);
}

void Scene::setupLookAtCamera() {
    //camera->setViewMatrixAsLookAt (const osg::Vec3d &eye, const osg::Vec3d &center, const osg::Vec3d &up);
}


int Scene::run() {
    // this is very similar to calling viewer->run()
    // without adding a default TackballManipulator
    // this is extracted from Viewer:run() and ViewerBase::run()
    _viewer->setReleaseContextAtEndOfFrameHint(false);

    if (!_viewer->isRealized()) {
        _viewer->realize();
    }

    // run simulation loop
    while (!_viewer->done()) {
        update();
        draw();
    }

    return 0;
}


// Accesssors
//
void Scene::setBackgroundColor( const osg::Vec4& color ) {
    _camera->setClearColor(color);
}
void Scene::setBackgroundColor( float r, float g, float b, float a ) {
    setBackgroundColor(osg::Vec4(r,g,b,1.0));
}

osg::Vec3 Scene::getBackgroundColor() {
    const osg::Vec4& col4 = _camera->getClearColor();
    osg::Vec3 col3 = osg::Vec3(col4[0], col4[1], col4[2]);
    return col3;
}

void Scene::setBackgroundAuto( bool bAuto ) {
    if( bAuto ) {
        _camera->setClearMask( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    } else {
        _camera->setClearMask(0);
    }
}

int Scene::getFrameNumber() {
    return _viewer->getViewerFrameStamp()->getFrameNumber();
}

double Scene::getSimulationTime() {
    return _viewer->getViewerFrameStamp()->getSimulationTime();
}

void Scene::enableDefaultLighting() {
    _viewer->setLightingMode(osg::View::SKY_LIGHT);
}
void Scene::disableDefaultLighting() {
    _viewer->setLightingMode(osg::View::NO_LIGHT);
}

void Scene::enableMultisampling( int val ) {
    osg::DisplaySettings::instance()->setNumMultiSamples(val);
}
void Scene::disableMultisampling() {
    osg::DisplaySettings::instance()->setNumMultiSamples(0);
}




// Scene Graph Hierarchy
//

bool Scene::addChild( Node* node ) {
    return _root->addChild(node->getOsgXForm().get());
}
bool Scene::insertChild( unsigned int index, Node* node ) {
    return _root->insertChild(index, node->getOsgXForm().get());
}
bool Scene::removeChild( Node* node ) {
    return _root->removeChild(node->getOsgXForm().get());
}
bool Scene::replaceChild( Node* origNode, Node* newNode ) {
    return _root->replaceChild(origNode->getOsgXForm().get(), newNode->getOsgXForm().get());
}
unsigned int Scene::getNumChildren() {
    return _root->getNumChildren();
}
bool Scene::setChild( unsigned int index, Node* node ) {
    return _root->setChild(index, node->getOsgXForm().get());
}
Node* Scene::getChild( unsigned int index ) {
    osg::Node* node = _root->getChild(index);
    Node* sxnode = dynamic_cast<Node*>(node->getUserData());
    if( !sxnode ) {
        osg::notify(osg::WARN) << "in getChild, not a Node at index" << std::endl;
    }
    return sxnode;
}
bool Scene::containsNode( Node* node ) {
    return _root->containsNode(node->getOsgXForm().get());
}
unsigned int Scene::getChildIndex( Node* node ) {
    return _root->getChildIndex(node->getOsgXForm().get());
}


// Scene Graph Geometry
//

bool Scene::addDrawable( Drawable* drawable ) {
    return _geode->addDrawable(drawable->getOsgDrawable().get());
}
bool Scene::removeDrawable( Drawable* drawable ) {
    return _geode->removeDrawable(drawable->getOsgDrawable().get());
}
bool Scene::replaceDrawable( Drawable* origDrawable, Drawable* newDrawable ) {
    return _geode->replaceDrawable(origDrawable->getOsgDrawable().get(), newDrawable->getOsgDrawable().get());
}
unsigned int Scene::getNumDrawables() {
    return _geode->getNumDrawables();
}
bool Scene::setDrawable( unsigned int index, Drawable* drawable ) {
    return _geode->setDrawable( index, drawable->getOsgDrawable().get());
}
Drawable* Scene::getDrawable( unsigned int index ) {
    osg::Drawable* drawable = _geode->getDrawable(index);
    Drawable* sxdrawable = dynamic_cast<Drawable*>(drawable->getUserData());
    if( !sxdrawable ) {
        osg::notify(osg::WARN) << "in getDrawable, not a Drawable at index" << std::endl;
    }
    return sxdrawable;
}
bool Scene::containsDrawable( Drawable* drawable ) {
    return _geode->containsDrawable(drawable->getOsgDrawable().get());
}
unsigned int Scene::getDrawableIndex( Drawable* drawable ) {
    return _geode->getDrawableIndex(drawable->getOsgDrawable().get());
}



void Scene::update() {
    if( _app != NULL ) {
        _app->update();
    }

    // extracted from viewer->frame()
    if (_viewer.valid()) {
        zIndexWidgetTraversal();
        _viewer->advance();
        _viewer->eventTraversal();
        _viewer->updateTraversal();
    }
}
void Scene::zIndexWidgetTraversal() {
    // This traversal makes sure zIndex is set in the order
    // of rendering (depth-first traversal) for all widgets
    _zIndexVisitor.reset(0);
    _root->accept(_zIndexVisitor);
}

void Scene::draw() {
    if (_viewer.valid()) {
        _viewer->renderingTraversals();
    }
}
void Scene::windowResized(int w, int h) {
}
void Scene::addEventHandler( App* app ) {
    _app = app;
    _viewer->addEventHandler(_app);
}




// Inject Touch Events
//

void Scene::mousePress( float x, float y, int button ) {
    theMouseTouch.setScreenPosition(x,y);
    theMouseTouch.id = button;
    touchDown(&theMouseTouch);
}
void Scene::touchDown( float x, float y, int touchId ) {
    // check if touch is really new
    if( _activeTouches.find(touchId) != _activeTouches.end() ) {
        osg::notify(osg::WARN) << "in touchDown(...), touchId collision" << std::endl;
    }
    // create touch
    Touch* touch = new Touch(x, y, touchId);
    _activeTouches[touchId] = touch;
    // delegate touchDown to relevant widgets
    touchDown(touch);
}
void Scene::touchDown( Touch* touch ) {
    // calclulate intersection
    // by shooting a ray from the mouse location through the scene
    rayIntersectWidgets(touch->getScreenX(), touch->getScreenY());  //populate touchHits
    // call node's event handler
    // widget with highest zIndex first
    bool consumed = false;
    std::map<int,TouchHit*>::reverse_iterator iter;
    for( iter = _touchHits.rbegin(); iter != _touchHits.rend(); ++iter ) {
        touch->widget = iter->second->widget;

        // We already have an intersection point in here:
        // -----------------------------------------------------
        // touch->setLocalPosition(iter->second->localPosition);
        // touch->setWorldPosition(iter->second->worldPosition);
        // -----------------------------------------------------
        // Unfortunately this isec point is slightly different from
        // what we get with simple plane intersection, latter being
        // less accurate. Still it's more important to be conistent
        // otherwise we get a slight bump when dragging widgets.
        // For this reason we recaclulate the isec point with the
        // same method as we will be using in touchMove and touchUp.
        // Intersect touch with the associated widget's plane (x-y plane).
        osg::Vec3d isect = rayIntersectWithXYPlane(touch->getScreenX(),
                                                   touch->getScreenY(),
                                                   touch->widget);
        touch->setLocalPosition(isect);

        //call touch event handler on the widget that
        //is associated with this touchHit, pass the touch
        //which is owned by activeTouches
        consumed = iter->second->widget->touchDownInternal( *touch );
        if(consumed) { break; }
    }
}


void Scene::mouseDrag( float x, float y, int button ) {
    theMouseTouch.setScreenPosition(x,y);
    theMouseTouch.id = button;
    touchMove(&theMouseTouch);
}
void Scene::touchMove( float x, float y, int touchId ) {
    // check if we really have this touch
    if( _activeTouches.find(touchId) == _activeTouches.end() ) {
        osg::notify(osg::WARN) << "in touchMove(...), invalid touchId" << std::endl;
    }
    // match touch
    Touch* touch = _activeTouches[touchId];
    touch->setScreenPosition(x,y);
    // delegate touchMove to relevant widgets
    touchMove(touch);
}
void Scene::touchMove( Touch* touch ) {
    if( touch->widget ) {
        // Intersect touch with the associated widget's plane (x-y plane).
        osg::Vec3d isect = rayIntersectWithXYPlane(touch->getScreenX(),
                                                   touch->getScreenY(),
                                                   touch->widget);
        touch->setLocalPosition(isect);
        touch->widget->touchMoveInternal( *touch );
    }
}


void Scene::mouseRelease( float x, float y, int button ) {
    theMouseTouch.setScreenPosition(x,y);
    theMouseTouch.id = button;
    touchUp(&theMouseTouch);
}
void Scene::touchUp( float x, float y, int touchId ) {
    // check if we really have this touch
    if( _activeTouches.find(touchId) == _activeTouches.end() ) {
        osg::notify(osg::WARN) << "in touchUp(...), invalid touchId" << std::endl;
    }
    // match touch
    Touch* touch = _activeTouches[touchId];
    touch->setScreenPosition(x,y);

    // delegate touchUp to relevant widgets
    touchUp(touch);

    // remove touch
    _activeTouches.erase(touch->getId());
    delete touch;
}
void Scene::touchUp( Touch* touch ) {
    if( touch->widget ) {
        // Intersect touch with the associated widget's plane (x-y plane).
        osg::Vec3d isect = rayIntersectWithXYPlane(touch->getScreenX(),
                                                   touch->getScreenY(),
                                                   touch->widget);
        touch->setLocalPosition(isect);
        touch->widget->touchUpInternal( *touch );
    }
}



// Private Methods
//

void Scene::rayIntersectWidgets( float x, float y ) {
    // return the widget node that is under the x,y screen position
    // for multiple hits return closest to the camera
    // for no hits return NULL

    // clear touchHits
    std::map<int,TouchHit*>::iterator iter;
    for( iter = _touchHits.begin(); iter != _touchHits.end(); ++iter ) {
        delete iter->second;
    }
    _touchHits.clear();

    osgUtil::LineSegmentIntersector::Intersections intersections;
    if( _viewer->computeIntersections(x,y,intersections) ){
        osgUtil::LineSegmentIntersector::Intersections::iterator  hitr;
        for(hitr = intersections.begin(); hitr != intersections.end(); ++hitr) {
            if (!hitr->nodePath.empty()) {
                osg::Referenced* node = hitr->nodePath.back()->getUserData();
                Widget* widget = dynamic_cast<Widget*>(node);
                if( widget ) {
                    TouchHit* touchHit = new TouchHit(
                        widget, osg::Vec2(x,y),
                        hitr->getLocalIntersectPoint(),
                        hitr->getWorldIntersectPoint()
                    );
                    _touchHits[widget->getZIndex()] = touchHit;
                }
            }
        }
    }
}


osg::Vec3d  Scene::rayIntersectWithXYPlane(float x, float y, Widget* widget) {
    // flip mouse y axis, only needed when embedded in glut (legecy)
    // y = height - y;

    osg::Vec3d isect = osg::Vec3d(0.0, 0.0, 0.0);

    // compute model to window transform
    // Model*View*Projection*WindowMatrix
    osg::Matrixd matrix;
    matrix.postMult(widget->getWorldTransform());
    matrix.postMult(_camera->getViewMatrix());
    matrix.postMult(_camera->getProjectionMatrix());
    matrix.postMult(_camera->getViewport()->computeWindowMatrix());

    osg::Matrixd inverse;
    inverse.invert(matrix);

    // Transform ray from window to model coordinates
    osg::Vec3d startRay = osg::Vec3d(x, y, 0.0) * inverse;
    osg::Vec3d endRay = osg::Vec3d(x, y, 1.0) * inverse;

    // Intersect ray with XY-plane
    // (0,0,1,0) is plane in hess normal form
    getPlaneLineIntersection(osg::Vec4d(0.0, 0.0, 1.0, 0.0), startRay, endRay, isect);

    return isect;
}
bool Scene::getPlaneLineIntersection(const osg::Vec4d& plane,
                                       const osg::Vec3d& lineStart, const osg::Vec3d& lineEnd,
                                       osg::Vec3d& isect)
{
    // intersects an infinite line with an infinite plane
    // in hesse normal form  Ax + By + Cz + D = 0
    // A,B,C is the normal vector of the plane, D the distance to the origin
    const double deltaX = lineEnd.x() - lineStart.x();
    const double deltaY = lineEnd.y() - lineStart.y();
    const double deltaZ = lineEnd.z() - lineStart.z();

    const double denominator = (plane[0]*deltaX + plane[1]*deltaY + plane[2]*deltaZ);
    if (! denominator) return false;

    const double C = (plane[0]*lineStart.x() + plane[1]*lineStart.y() + plane[2]*lineStart.z() + plane[3]) / denominator;

    isect.x() = lineStart.x() - deltaX * C;
    isect.y() = lineStart.y() - deltaY * C;
    isect.z() = lineStart.z() - deltaZ * C;

    return true;
}


